; Copyright (c) 2009 Charl P. Botha <http://cpbotha.net/>
; All rights reserved.

; Redistribution and use in source and binary forms, with or without
; modification, are permitted provided that the following conditions
; are met:
; 1. Redistributions of source code must retain the above copyright
;    notice, this list of conditions and the following disclaimer.
; 2. Redistributions in binary form must reproduce the above copyright
;    notice, this list of conditions and the following disclaimer in the
;    documentation and/or other materials provided with the distribution.
; 3. The name of the author may not be used to endorse or promote products
;    derived from this software without specific prior written permission.

; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
; IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


; todo:
; * if you invoke the path editor with F3 and apply, the cursor position
;   in the main gui is lost.

; start of an envedit clone
; why am I still doing this?

; notes:
; you need to do 
; win32gui.SendMessageTimeout(
;  win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0,
;  'Environment', win32con.SMTO_ABORTIFHUNG, 100)        
; to get new windows to recognise the new settings
; nearest I can get in AHK:
; SendMessage, 0x1A, 0, Environment,, ahk_id 0xFFFF
; with 0x1A being WM_SETTINGCHANGE
; 0 being wparam (as above)
; Environment being lparam (as above)
; ahk_id 0xFFFF being the broadcast
; above: flags includes ABORTIFHUNG, timeout is 100


version := "9.11.1"
window_title = envedit %version% Environment Editor (c) 2009 by Charl P. Botha

; new instances can be started up
#SingleInstance OFF

; functions =========================================================

; you need to pass string literals to this function
EnvToText(RootKey, Key)
{
; need to loop through whole registry and gather all keys and values 
; append to variable and show in edit.  This thing will loop through both 
; keys and values separately
; string literals have been passed, so get to contents with %% 
Loop, %RootKey%, %Key%, 1, 0
{
  if a_LoopRegType = key
    value =
  else
  {
    RegRead, value
    if ErrorLevel
      value = *error*
  }
  ; %a_LoopRegType% is e.g. REG_SZ
  regline = %a_LoopRegName% = %value%
  if regstr
    regstr = %regstr%`r`n%regline%
  else
    regstr = %regline%
}

Sort, regstr
StringReplace, regstr, regstr, `r`n, `r`n`r`n, All

return regstr
}

; pass string literals as params
TextToEnv(Text, RootKey, Key)
{
; loop through text, building up list of keys that we can use to
; determine which keys from the registry should be deleted
alivekeys =
Loop, PARSE, Text, `n, `r
{
; if line is empty, skip on to next one
if A_LoopField =
  continue

; also skip lines that have no equals
if not InStr(A_LoopField, "=")
  continue 

; else try to split it
StringSplit, keyvalue, A_LoopField, `=
if keyvalue1
  {
    ; autotrim will remove leading / trailing spaces
    akey = %keyvalue1%
    StringUpper, akey, akey
    avalue = %keyvalue2%

    if alivekeys =
      alivekeys = %akey%
    else
      alivekeys = %alivekeys%,%akey%
  }
}
;MsgBox %alivekeys%

; now go through registry, deleting keys that are NOT in our list
; the registry loop goes in reverse order, neat huh?
Loop, %RootKey%, %Key%, 1, 0
{
  ; we don't want subkeys
  if a_LoopRegType = key
    continue

  ; if the current reg key is not in alivekeys, we kill it
  StringUpper, A_LoopRegNameU, A_LoopRegName 
  if A_LoopRegNameU not in %alivekeys%
    RegDelete
}

; loop through the text one last time, this time applying all values
Loop, PARSE, Text, `n, `r
{
; if line is empty, skip on to next one
if A_LoopField =
  continue

; also skip lines that have no equals
if not InStr(A_LoopField, "=")
  continue 

; else try to split it
StringSplit, keyvalue, A_LoopField, `=
if keyvalue1
  {
    ; autotrim will remove leading / trailing spaces
    akey = %keyvalue1%
    StringUpper, akey, akey
    avalue = %keyvalue2%

    IfInString, avalue, `%
      rtype = REG_EXPAND_SZ
    else
      rtype = REG_SZ

    RegWrite, %rtype%, %RootKey%, %Key%, %akey%, %avalue%
  }
}

; notify everyone that we've changed the environment
SendMessage, 0x1A, 0, "Environment",, ahk_id 0xFFFF

; we should probably read back the environment to see what
; we've in fact done...
regstr := EnvToText(RootKey, Key)
GuiControl,,MainEdit,%regstr%
}


; get selection start char in c1 and end char in c2 for
; control Hwnd
GetSel(Hwnd, byref c1, byref c2)
{
; EM_GETSEL returns start and end char of selection.  Even
; if there's no selection, both will be the caret.
; http://msdn.microsoft.com/en-us/library/bb775458(VS.85).aspx
VarSetCapacity(A1,4)
VarSetCapacity(A2,4)
SendMessage, 0xB0, &A1, &A2,, ahk_id %Hwnd%
c1 := *(&A1)+(*(&A1+1)<<8)+(*(&A1+2)<<16)+(*(&A1+3)<<24)    ; Selection start
c2 := *(&A2)+(*(&A2+1)<<8)+(*(&A2+2)<<16)+(*(&A2+3)<<24)
}

SetSel(Hwnd, c1, c2)
{
SendMessage, 0xB1, %c1%, %c2%, , ahk_id %Hwnd%
}

; auto-execute section ==============================================
; until the first return. we use this to build the gui.


hkcustr := EnvToText("HKCU", "Environment")
; vMainEdit assigns edit control to var "MainEdit"
Gui, Add, Edit, w550 h300 vMainEdit -Wrap +HScroll
; also store Hwnd of edit control, we need this for some calls
GuiControlGet, Hwnd_MainEdit, Hwnd, MainEdit

Menu, FileMenu, Add, &Save to TXT File	Ctrl+S, MenuFileSave
Menu, FileMenu, Add, &Load from TXT File	Ctrl+O, MenuFileLoad
Menu, FileMenu, Add
Menu, FileMenu, Add, E&xit	Ctrl+Q, GuiClose

Menu, EnvMenu, Add, S&ync with Environment (UNDO)	F5, MenuEnvSync
Menu, EnvMenu, Add, &Apply to Environment	F8, MenuEnvApply 

Menu, ToolsMenu, Add, &Path edit current variable	F3, MenuToolsPathEdit
Menu, ToolsMenu, Add, Insert &directory at cursor, MenuToolsInsDir
Menu, ToolsMenu, Add, Insert &file path at cursor, MenuToolsInsFilePath

Menu, HelpMenu, Add, &About, MenuHelpAbout

Menu, MyMenuBar, Add, &File, :FileMenu
Menu, MyMenuBar, Add, E&nvironment, :EnvMenu
Menu, MyMenuBar, Add, &Tools, :ToolsMenu
Menu, MyMenuBar, Add, &Help, :HelpMenu
Gui, Menu, MyMenuBar

Gui, Show,, %window_title%

; only set text here AFTER window has been shown
; else all text gets selected by default
GuiControl,,MainEdit, %hkcustr% 

; going to use this later for setting up shortcut keys
GroupAdd, eeGroup, %window_title%

; already create path edit dialog, don't use yet:
Gui, 2: +owner
Gui, 2: Add, Edit, w400 h300 vPathEdit -Wrap +HScroll
GuiControlGet, Hwnd_PathEdit, 2: Hwnd, PathEdit
; and set the default selection
SetSel(Hwnd_PathEdit, 0,0)
Gui, 2: Add, Button,,Apply
Gui, 2: Add, Button,x+10,Cancel

Menu, peMenuBar, Add, &Alpha Sort, peMenuAlphaSort
Menu, peMenuBar, Add, Insert &Directory, peMenuInsDir
Menu, peMenuBar, Add, Insert &File Path, peMenuInsFilePath
Gui, 2: Menu, peMenuBar

Return

; event handlers ====================================================
#IfWinActive ahk_group eeGroup
^s::
MenuFileSave: ;------------------------------------------------------
Gui +OwnDialogs
FileSelectFile, SelectedFileName, S16,,Save File, Text Documents (*.txt)
if SelectedFileName =
  return

IfExist %SelectedFileName%
{
    FileDelete %SelectedFileName%
    if ErrorLevel
    {
        MsgBox The attempt to overwrite "%SelectedFileName%" failed.
        return
    }
}
; this will by default get stuff from the control called MainEdit into a
; local variable MainEdit (confusing, I know)
GuiControlGet, MainEdit
FileAppend, %MainEdit%, %SelectedFileName%  ; Save the contents to the file.

return

#IfWinActive ahk_group eeGroup
^o::
^l::
MenuFileLoad: ;------------------------------------------------------
Gui +OwnDialogs
FileSelectFile, SelectedFileName, 1,, Load File, Text Documents (*.txt)
if SelectedFileName =
  return
FileRead, Contents, %SelectedFileName%
GuiControl,, MainEdit, %Contents%

return

#IfWinActive ahk_group eeGroup
F5::
MenuEnvSync: ;-------------------------------------------------------
hkcustr := EnvToText("HKCU", "Environment")
GuiControl,, MainEdit, %hkcustr%

return

#IfWinActive ahk_group eeGroup
F8::
MenuEnvApply: ;------------------------------------------------------
GuiControlGet, MainEdit
TextToEnv(MainEdit, "HKCU", "Environment")

return

#IfWinActive ahk_group eeGroup
F3::
MenuToolsPathEdit: ;-------------------------------------------------
; get current line from MainEdit control
ControlGet, CurLineN, CurrentLine, , , ahk_id %Hwnd_MainEdit%
ControlGet, CurLine, Line, %CurLineN%, , ahk_id %Hwnd_MainEdit%

; only process line if it has an "='
if not InStr(CurLine, "=")
  return

; break the line into key-value array
StringSplit, keyvalue, CurLine, `=
if keyvalue1 =
  return

; autotrim will remove leading / trailing spaces
CurKey = %keyvalue1%
value = %keyvalue2%

; variable is global, so have to zero
complist =
; the omitchars at the end make sure everything is stripped!
Loop, PARSE, value, `;, %A_Space%%A_Tab%
{
if complist =
  complist = %A_LoopField%
else
  complist = %complist%`n%A_LoopField%
}

Gui +disabled ; disable parent gui, we want modal
Gui, 2: Show,, Edit: %CurKey%

; default window is the main, we have to specify
; sub-window explicitly here
; we set the text AFTER the window has been shown, else
; all gets selected by default
GuiControl,2:,PathEdit, %complist%

return

MenuToolsInsDir: ;---------------------------------------------------
Gui +OwnDialogs
FileSelectFolder, TheFolder
if TheFolder =
  return
; then use that to paste our text at the current cursor
Control, EditPaste, %TheFolder%, , ahk_id %Hwnd_MainEdit%
return

MenuToolsInsFilePath: ;----------------------------------------------
Gui +OwnDialogs

; have to restore this after the fileselect dialog, it nukes the selection
GetSel(Hwnd_MainEdit, c1, c2)

FileSelectFile, SelectedFileName,,, Load File, All (*.*)

; restore selection (somehow, fileselect dialog selects all text)
SetSel(Hwnd_MainEdit, c1, c2)

if SelectedFileName =
  return

Control, EditPaste, %SelectedFileName%, , ahk_id %Hwnd_MainEdit%
return


MenuHelpAbout: ;-----------------------------------------------------
; owndialogs: dialogs are modal.
Gui, +OwnDialogs
MsgBox, 64, Help | About,
(
envedit %version% is copyright 2009 by Charl P. Botha <cpbotha.net> 

Because the Windows environment editor will ruin your day, every time.

YOU ARE USING ENVEDIT ENTIRELY **AT YOUR OWN RISK**.
)
return

peMenuAlphaSort: ;---------------------------------------------------
; GUI 2 is already default here (they trigged us)
; get text from edit
GuiControlGet, PathEdit
; sort it
Sort, PathEdit
; put back in edit
GuiControl, , PathEdit, %PathEdit%
return

peMenuInsDir: ;------------------------------------------------------
; needs to be modal
Gui +OwnDialogs
FileSelectFolder, TheFolder
if TheFolder =
  return
; then use that to paste our text at the current cursor
Control, EditPaste, %TheFolder%, , ahk_id %Hwnd_PathEdit%
return

peMenuInsFilePath: ;-------------------------------------------------
; Gui 2 should be default in this thread, make sure the file
; selection dialog is modal
Gui +OwnDialogs

; have to restore this after the fileselect dialog, it nukes the selection
GetSel(Hwnd_PathEdit, c1, c2)

FileSelectFile, SelectedFileName

; restore selection (somehow, fileselect dialog selects all text)
SetSel(Hwnd_PathEdit, c1, c2)

if SelectedFileName =
  return
; then use that to paste our text at the current cursor
; hmmm, when this comes back from the fileselection dialog, all text
; is selected, which then gets overwrited by the new path.
; apparently default behaviour for edit controls: selection is inverted
; when they get focus.  Doh.
Control, EditPaste, %SelectedFileName%, , ahk_id %Hwnd_PathEdit%
return

; automatic label: button "Apply" in 2nd GUI.  just wow.
2ButtonApply: ;------------------------------------------------------
; concatenate line with ; instead of `n (use Loop)
; then do %CurKey% = concatenated_string
; then loop through current MainEdit text putting into new textvar
; when you reach %CurLineN%, do the new string
; copy the whole text into the MainEdit
; should we reset caret?
GuiControlGet, PathEdit
; loop through text, splitting on line ends
; `r in OmitChars means we work on all line-endings
; other two OmitChars strips leading and trailing space
NewValue =
Loop, Parse, PathEdit, `n, `r%A_Space%%A_Tab%
{
  ; only do stuff if there is stuff
  if A_LoopField =
    continue

  if NewValue =
    NewValue = %A_LoopField%
  else
    NewValue = %NewValue%`;%A_LoopField% 
}
NewKeyValueString = %CurKey% = %NewValue%

; put this string back in the MainEdit control
; first get the current text
GuiControlGet, MainEdit, 1:
; now loop throuh it, replacing JUST the right string
NewText = 
Loop, Parse, MainEdit, `n, `r
{
; HAVE to use these parentheses for the expression to get evaluated
if (A_Index = CurLineN)
  TheLine = %NewKeyValueString%
else
  TheLine = %A_LoopField%

if NewText =
  NewText = %TheLine%
else
  NewText = %NewText%`n%TheLine%
}

GuiControl,1:,MainEdit, %NewText%

GoSub, 2GuiClose
return

2ButtonCancel: ;-----------------------------------------------------
GoSub, 2GuiClose
return

; same here, automatic label
2GuiClose: ;---------------------------------------------------------
; in this thread, Gui 2 is the default, so we have to specify Gui 1 to
; get to the main window and re-enable it.
Gui 1: -Disabled
; put it away for later.  hehe.
Gui 2: Hide
return

#IfWinActive ahk_group eeGroup
^q::
GuiClose: ;----------------------------------------------------------
ExitApp
